# coding=utf-8
import json
import os.path
import sys
import re
import threading
import time

try:
  import edge_tts as et
except Exception as ex:
  print(
    '''
    获取朗读接口失败，请按照下列提示操作：
    终端执行：pip3 install edge_tts
    命令执行完毕后重新运行本脚本
    '''
  )
  exit(0)
import subprocess
# from playsound import playsound
import asyncio
import platform
"""
准备工作：
1、pip3 install edge_tts
2、pip3 install playsound
*若2报错，可选：pip3 install --upgrade wheel

windows系统下的额外操作
将源码中command = ' '.join(command).encode('utf-16')变为command = ' '.join(command)即可
"""


sysName = platform.system()

try:
  if sysName == 'Windows' or '-w' in sys.argv:
    from playsound import playsound
    """
    sudo apt install python3-gst-1.0
    树莓派额外使用
    """
  else:
    def playsound(p):
      resultCode = subprocess.call('play-audio {}'.format(p), shell=True)
      if resultCode != 0:
        print(
          '''
          获取朗读接口失败，请按照下列提示操作：
          
          termux 终端执行：pkg install play-audio
          
          其它终端请在命令参数中添加：-w
          '''
        )
        exit(0)
except Exception as ex:
  print('''
  获取朗读接口失败，请按照下列提示操作：
  
  
  终端执行：pip3 install playsound
  * 执行报错可尝试在终端执行：pip3 install --upgrade wheel

  
  命令执行完毕后重新运行本脚本
  ''')
  exit(0)


def RTxt(path):
  with open(path, encoding='utf8') as f:
    return f.read()


def WTxt(path, content):
  with open(path, 'w', encoding='utf8') as f:
    f.write(content)


def FilterLine(line):
  # print('line=', line)
  line = re.sub(r'^(.*?)\d*\s*$', r'\1', line)
  line = re.sub(r'&.*?;', '', line)
  # print('line2=', line)
  return line


class Reader:
  def __init__(self, txtPath, voice=None, startPos=-1, rate='+25%', volume='+0%', jump=False):
    self.rate = rate
    self.volume = volume
    self.novel = RTxt(txtPath)
    self.novelName = os.path.basename(txtPath).split('.')[0]
    self.lines = self.novel.split('\n')
    print('朗读速度=', self.rate)
    print('朗读音量=', self.volume)
    print('书籍行数=', len(self.lines))
    self.allSetting = {}
    self.sayProcess = None
    self.errorEnd = False
    if os.path.exists('setting.json'):
      try:
        self.allSetting = json.loads(RTxt('setting.json'))  # type: dict
      except json.decoder.JSONDecodeError:
        self.allSetting = {}
    self.end = False
    if not os.path.exists('temp'):
      os.mkdir('temp')
    self.allSetting['lastNovel'] = self.novelName
    self.novelTempPath = 'temp/{}'.format(self.novelName)
    if not os.path.exists(self.novelTempPath):
      os.mkdir(self.novelTempPath)
    if voice is not None:
      if voice != self.voice:
        print('更换声优，重置下载进度……', self.setting['downloadPos'])
        self.DelTemp(self.setting['pos'], self.setting['downloadPos'])
        self.setting['downloadPos'] = self.setting['pos']
        print('重置完毕！', self.setting['downloadPos'])
      self.setting['voice'] = voice
    if startPos > -1:
      if jumpSpeak and startPos <= self.setting['pos']:
        print(f'起读行{startPos}低于当前行{self.setting["pos"]}，不额外设定起读行……')
        return
      print('已设定起读行：', startPos)
      self.setting['downloadPos'] = startPos
      self.setting['pos'] = startPos

  def DelTemp(self, startPos, endPos):
    for pos in range(startPos, endPos):
      mp3Path = '{}/{}.mp3'.format(self.novelTempPath, pos)
      if os.path.exists(mp3Path):
        os.remove(mp3Path)

  @property
  def voice(self):
    return self.setting.setdefault('voice', 'zh-CN-YunxiNeural')

  @property
  def setting(self) -> dict:
    # noinspection PyTypeChecker
    return self.allSetting.setdefault(self.novelName, {'downloadPos': 0, 'pos': 0})

  def Start(self):
    def Download():
      while self.setting['downloadPos'] < len(self.lines) and not self.errorEnd:
        line = self.lines[self.setting['downloadPos']]
        line = FilterLine(line)
        try:
          downloadPos = self.setting['downloadPos']
          self.setting['downloadPos'] += 1
          self.Download(line, downloadPos)

        except Exception as e:
          print('Download.e=', e, self.setting['downloadPos'])
        WTxt('setting.json', json.dumps(self.allSetting, indent=2, ensure_ascii=False))
        while self.setting['downloadPos'] > self.setting['pos'] + 300:
          time.sleep(1)

    def Say():
      while self.setting['pos'] < len(self.lines) and (exitTime < 0 or exitTime > time.time()) and not self.errorEnd:
        while self.setting['downloadPos'] < self.setting['pos'] + 10 and self.setting['downloadPos'] < len(self.lines):
          print('等待缓存中……')
          time.sleep(1)
        # print('下载行=', self.setting['downloadPos'])
        # print('朗读行=', self.setting['pos'])
        print(
          '朗读行=', self.lines[self.setting['pos']], self.setting['pos'], self.setting['downloadPos'], len(self.lines)
        )
        self.Say()
        self.setting['pos'] += 1

    if offlineRead:
      self.OfflineSay()
      return
    for _ in range(downloadThreadCount):
      downloadThread = threading.Thread(target=Download, daemon=True)
      downloadThread.start()

    sayThread = threading.Thread(target=Say, daemon=True)
    sayThread.start()
    sayThread.join()

  def OfflineSay(self):
    try:
      import pyttsx3
    except Exception as e:
      print(
        """
        未安装依赖，请使用下列指令安装后重试！
        pip3 install pyttsx3
        """
      )
      return
    speakerEngine = pyttsx3.init()
    speakerEngine.setProperty('rate', 320)
    while self.setting['pos'] < len(self.lines) and (exitTime < 0 or exitTime > time.time()) and not self.errorEnd:
      print(
        '朗读行=', self.lines[self.setting['pos']], self.setting['pos'], self.setting['downloadPos'], len(self.lines)
      )
      text = self.lines[self.setting['pos']]
      if re.findall(u'[\u4e00-\u9fa5]+', text):
        speakerEngine.say(text)
        speakerEngine.runAndWait()
      self.setting['pos'] += 1
      WTxt('setting.json', json.dumps(self.allSetting, indent=2, ensure_ascii=False))

  def Download(self, text, downloadPos, deep=10):
    if deep < 1:
      print('[error]下载[{}]超出最大预设次数，程序终止！'.format(downloadPos))
      self.errorEnd = True
      return
    mp3Path = '{}/{}.mp3'.format(self.novelTempPath, downloadPos)
    try:
      if os.path.exists(mp3Path) and os.path.getsize(mp3Path) > 0:
        return
      if text == '' or not re.findall(u'[\u4e00-\u9fa5]+', text):
        if len(text) > 0:
          print('无效下载行=', downloadPos, text)
        return

      async def Do():
        await t.save(mp3Path)
        if not os.path.exists(mp3Path) or os.path.getsize(mp3Path) == 0:
          self.Download(text, downloadPos, deep - 1)
      t = et.Communicate(text=text, voice=self.voice, rate=self.rate, volume=self.volume)
      asyncio.run(Do())
    except Exception as e:
      print('[error]Download.e=', e, mp3Path, deep)
      time.sleep(2)
      self.Download(text, downloadPos, deep-1)

  def Say(self, deep=20):
    """朗读"""
    mp3Path = '{}/{}.mp3'.format(self.novelTempPath, self.setting['pos'])
    if os.path.exists(mp3Path) and os.path.getsize(mp3Path) > 0:
      try:
        playsound('{}/{}'.format(os.getcwd(), mp3Path).replace('/', os.path.sep))
      except Exception as e:
        print('[error]Say.e=', e, '\nos.path.getsize(mp3Path)=', os.path.getsize(mp3Path)), 'deep=', deep
        if deep > 0:
          time.sleep(0.2)
          self.Say(deep-1)


voiceConfig = {
  'zh-CN-XiaoxiaoNeural': 'Female',
  'zh-CN-XiaoyiNeural': 'Female',
  'zh-CN-YunjianNeural': 'Male',
  'zh-CN-YunxiNeural': 'Male',
  'zh-CN-YunxiaNeural': 'Male',
  'zh-CN-YunyangNeural': 'Male',
  'zh-CN-liaoning-XiaobeiNeural': 'Female',
  'zh-CN-shaanxi-XiaoniNeural': 'Female',
  'zh-HK-HiuGaaiNeural': 'Female',
  'zh-HK-HiuMaanNeural': 'Female',
  'zh-HK-WanLungNeural': 'Male',
  'zh-TW-HsiaoChenNeural': 'Female',
  'zh-TW-HsiaoYuNeural': 'Female',
  'zh-TW-YunJheNeural': 'Male',
  'zu-ZA-ThandoNeural': 'Female',
  'zu-ZA-ThembaNeural': 'Male',
}


def GetArgs(key, default=None):
  if key not in args:
    return default
  vIndex = args.index(key)
  if vIndex + 1 < len(args):
    return args[vIndex + 1]
  return default


def PaddingNovelPath(novelName):
  if '.' not in os.path.basename(novelName):
    novelName += '.txt'
  if 'novel/' not in novelName and not os.path.exists(novelName) and os.path.exists('novel/' + novelName):
    novelName = 'novel/' + novelName
  return novelName


def ReadSettingNovalPath():
  if os.path.exists('setting.json'):
    try:
      setting = json.loads(RTxt('setting.json'))
      if 'lastNovel' not in setting:
        print('缺少小说路径参数……\n例如：python main.py novel/xxxx.txt')
        exit(0)
      return PaddingNovelPath(setting['lastNovel'])
    except json.decoder.JSONDecodeError:
      print('缺少小说路径参数……\n例如：python main.py novel/xxxx.txt')
      exit(0)
  else:
    print('缺少小说路径参数……\n例如：python main.py novel/xxxx.txt')
    exit(0)


if __name__ == '__main__':
  if '-h' in sys.argv:
    print('''
    python3 main.py 小说路径
    朗读指定路径小说
    python3 main.py 小说路径 -o 1
    离线朗读指定路径小说

    python3 main.py 小说路径 -p 行号
    朗读指定路径小说，从某行开始

    python3 main.py 小说路径 -j 行号
    朗读指定路径小说，如果低于某行则从某行开始

    python3 main.py 小说路径 -t 分钟数
    朗读指定路径小说，多少分钟后结束

    python3 main.py 小说路径 -w
    使用playsound朗读指定路径小说；对于非termux的linux系统应该加此参数

    python3 main.py -h
    显示命令提示

    python3 main.py 小说路径 -c 下载线程数量
    指定小说语音下载线程数量，数量越多理论上越不容易出现断断续续的情况，默认3线程

    python3 main.py 小说路径 [-w] -v 声优名字
    设置指定声优播放小说（设置会储存，下次阅读本书命令不用再带-v）
    声优名字: 声优性别
    zh-CN-XiaoxiaoNeural: Female
    zh-CN-XiaoyiNeural: Female
    zh-CN-YunjianNeural: Male
    zh-CN-YunxiNeural: Male
    zh-CN-YunxiaNeural: Male
    zh-CN-YunyangNeural: Male
    zh-CN-liaoning-XiaobeiNeural: Female
    zh-CN-shaanxi-XiaoniNeural: Female
    zh-HK-HiuGaaiNeural: Female
    zh-HK-HiuMaanNeural: Female
    zh-HK-WanLungNeural: Male
    zh-TW-HsiaoChenNeural: Female
    zh-TW-HsiaoYuNeural: Female
    zh-TW-YunJheNeural: Male
    zu-ZA-ThandoNeural: Female
    zu-ZA-ThembaNeural: Male

    因为程序的一些逻辑，推荐将小说放在同目录的novel文件夹下，启用命令时可以忽略"novel/"
    '''.format(
      '\n'.join(['    {}: {}'.format(_k, _v) for _k, _v in voiceConfig.items()])
    ))
    exit(0)
  if not os.path.exists('novel'):
    os.mkdir('novel')
  args = sys.argv
  print('输入命令=', args)
  novelPath = ''
  if len(args) < 2:
    novelPath = ReadSettingNovalPath()
  else:
    novelPath = PaddingNovelPath(args[1])
    if not os.path.exists(novelPath):
      novelPath = ReadSettingNovalPath()
  novelVoice = GetArgs('-v')
  if novelVoice not in voiceConfig:
    novelVoice = None
  speakVolume = GetArgs('-s', '+0%')
  speakRate = GetArgs('-r', '+25%')
  speakStartPos = int(GetArgs('-p', -1))
  jumpSpeak = False
  if speakStartPos == -1:
    speakStartPos = int(GetArgs('-j', -1))
    jumpSpeak = True
  if GetArgs('-t', -1) == -1:
    exitTime = -1
  else:
    exitTime = int(GetArgs('-t', -1)) * 60 + time.time()
  downloadThreadCount = int(GetArgs('-c', '3'))
  print('加载小说: {}'.format(novelPath))
  print('朗读者: {}'.format(novelVoice))
  print('起读行: {}'.format(speakStartPos))
  print('终止时间: {}'.format(exitTime))
  offlineRead = GetArgs('-o', '0') != '0'
  reader = Reader(novelPath, novelVoice, speakStartPos, speakRate, speakVolume, jumpSpeak)
  reader.Start()
  print('程序终止！')
